﻿/**

 * Copyright (c) 2015-2017, FastDev 刘强 (fastdev@163.com).

 *

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *      http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */


using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OF.ILModifier.Entity;
using OF.ILModifier.Common;
using OF.ILModifier.Extension;

namespace OF.ILModifier.Handler
{
    public class SpecMethodReplaceHandler
    {
        private AssemblyHelper asmHelper;
        public SpecMethodReplaceHandler(AssemblyHelper asmHelper)
        {
            this.asmHelper = asmHelper;
        }

        public void WrapMethod(TypeDefinition targetType, MethodDefinition eventInvoker, 
            MethodInfo beforeMethod, MethodInfo replaceMethod, MethodInfo afterMethod)
        {
            if (beforeMethod != null && !beforeMethod.IsStatic)
            {
                throw new NotSupportedException("PreInvoke method only support for static!");
            }

            if (afterMethod != null && !afterMethod.IsStatic)
            {
                throw new NotSupportedException("AfterInvoke method only support for static!");
            }
            MethodDefinition rawInvoker = eventInvoker.CopyEmptyBody(eventInvoker.Name + DateTime.Now.ToString("yyyyMMddHHmmssfff"));
            targetType.Methods.Add(rawInvoker);
            eventInvoker.SetVariables(eventInvoker.Module, rawInvoker);

            Tuple<MethodInfo, MethodReference, MethodInfo, MethodInfo> methodCallTuple = new Tuple<MethodInfo, MethodReference, MethodInfo, MethodInfo>(beforeMethod, eventInvoker, replaceMethod, afterMethod);
            Instruction[] callInstructions = this.CreateEventWrapMethod(targetType, methodCallTuple, rawInvoker);
            ILProcessor oldProcessor = eventInvoker.Body.GetILProcessor();

            ILProcessor rawProcessor = rawInvoker.Body.GetILProcessor();
            foreach (var ins in eventInvoker.Body.Instructions.ToList())
            {
                rawProcessor.Append(ins);
                oldProcessor.Remove(ins);
            }
            eventInvoker.InitReturnVariable();
            foreach (var ins in callInstructions)
            {
                oldProcessor.Append(ins);
            }
        }

        private Instruction[] CreateEventWrapMethod(TypeDefinition targetType, Tuple<MethodInfo, MethodReference, MethodInfo, MethodInfo> methodCallTuple, MethodDefinition rawInvoker)
        {
            MethodReference replaceMethodDefine = null;
            if (methodCallTuple.Item2 == null)
            {
                throw new NotSupportedException("The method to be invoked param Item2 can't be null!");
            }

            Action<string, MethodReference> checkInvokeAction = (invokeName, method) =>
            {
                if (method != null)
                {
                    if (method.Parameters.Count != 1)
                    {
                        throw new NotSupportedException(invokeName + " can only have one param with type [" + AssemblyHelper.ParamContainerTypeName + "]");
                    }
                    else
                    {
                        if (!method.Parameters[0].ParameterType.GetFullName().Equals(asmHelper.ParamContainerFullTypeName))
                        {
                            throw new NotSupportedException(invokeName + " can only have one param with type [" + AssemblyHelper.ParamContainerTypeName + "]");
                        }
                    }
                }
            };
            MethodReference preMethod = asmHelper.ImportReference(methodCallTuple.Item1);
            MethodReference afterMethod = asmHelper.ImportReference(methodCallTuple.Item4);
            checkInvokeAction("PreInvoke", preMethod);
            checkInvokeAction("AfterInvoke", afterMethod);
            string errorMessage = "PreInvoke/Invoke/ReplaceInvoke/AfterInvoke must have the same method Signature!(PreInvoke/AfterInvoke/ReplaceInvoke must be static method, and should have one more param at begin to receive the instance object when Invoke is instance method, PreInvoke/AfterInvoke can't have return value, ReplaceInvoke return value keep same as Invoke)";
            List<TypeReference> paramTypeList = new List<TypeReference>();
            List<List<ParameterDefinition>> paramList = new List<List<ParameterDefinition>>();

            Action initReplaceMethod = () => {
                replaceMethodDefine = ILHelper.ToDefinition(methodCallTuple.Item3);// this.ImportReference(replaceMethod);
                if (replaceMethodDefine.IsGenericInstance)
                {
                    MethodHelper.CloneGenericParameters(replaceMethodDefine, (MethodDefinition)methodCallTuple.Item2);
                }
                AddReplaceWrapMethod(targetType, ref replaceMethodDefine, (MethodDefinition)methodCallTuple.Item2, rawInvoker);
            };

            int paramsCount = methodCallTuple.Item2.Parameters.Count;
            if (methodCallTuple.Item2 is MethodDefinition
                && !((MethodDefinition)methodCallTuple.Item2).IsStatic)
            {
                paramsCount = paramsCount + 1;
                paramTypeList.Add(targetType);
                paramList.Add(methodCallTuple.Item2.Parameters.ToList());

                if (methodCallTuple.Item3 != null)
                {
                    initReplaceMethod();
                    paramList.Add(replaceMethodDefine.Parameters.ToList());
                }
            }
            else
            {
                paramList.Add(methodCallTuple.Item2.Parameters.ToList());
                if (methodCallTuple.Item3 != null)
                {
                    initReplaceMethod();
                    paramList.Add(replaceMethodDefine.Parameters.ToList());
                }
            }
            paramTypeList.AddRange(methodCallTuple.Item2.Parameters.Select(prm => prm.ParameterType).ToList());
            CheckMethodParams(replaceMethodDefine, paramList, methodCallTuple, errorMessage);

            List<Instruction> result = new List<Instruction>((paramsCount + 1) * 3 + 1);
            ILProcessor worker = ((MethodDefinition)methodCallTuple.Item2).Body.GetILProcessor();

            if (preMethod != null)
            {
                MethodHelper.AddStaticCall(asmHelper, worker, paramTypeList, result, preMethod);
            }
            MethodReference itemInvoke = null;
            if (replaceMethodDefine == null)
            {
                itemInvoke = rawInvoker;//methodCallTuple.Item2;
            }
            else
            {
                itemInvoke = replaceMethodDefine;
            }

            MethodHelper.AppendCallIns(worker, paramsCount, result, itemInvoke, ((MethodDefinition)methodCallTuple.Item2));
            if (itemInvoke.HasReturnValue())
            {
                Instruction insCall = worker.Create(OpCodes.Stloc_0);
                result.Add(insCall);
            }

            if (afterMethod != null)
            {
                MethodHelper.AddStaticCall(asmHelper, worker, paramTypeList, result, afterMethod);
            }

            if (itemInvoke.HasReturnValue())
            {
                Instruction insCall = worker.Create(OpCodes.Ldloc_0);
                result.Add(insCall);
            }
            result.Add(worker.Create(OpCodes.Ret));
            return result.ToArray();
        }

        public void CheckMethodParams(MethodReference replaceMethodDefine, List<List<ParameterDefinition>> paramList, Tuple<MethodInfo, MethodReference, MethodInfo, MethodInfo> methodCallTuple, string errorMessage)
        {
            for (int i1 = 0; i1 < paramList.Count - 1; i1++)
            {
                if (!ILHelper.IsParamsEqual(paramList[i1], paramList[i1 + 1]))
                {
                    throw new NotSupportedException(errorMessage);
                }
            }

            if (methodCallTuple.Item1 != null)
            {
                if (ILHelper.HasReturnValue(methodCallTuple.Item1.ReturnType))
                {
                    throw new NotSupportedException("PreInvoke can't have return value");
                }
            }

            if (methodCallTuple.Item4 != null)
            {
                if (ILHelper.HasReturnValue(methodCallTuple.Item4.ReturnType))
                {
                    throw new NotSupportedException("AfterInvoke can't have return value");
                }
            }

            if (replaceMethodDefine != null)
            {
                if (methodCallTuple.Item2.HasReturnValue())
                {
                    if (!replaceMethodDefine.HasReturnValue())
                    {
                        throw new NotSupportedException(errorMessage);
                    }
                    if (methodCallTuple.Item2.ReturnType.GetFullName() != replaceMethodDefine.ReturnType.GetFullName())
                    {
                        throw new NotSupportedException(errorMessage);
                    }
                }
                else
                {
                    if (replaceMethodDefine.HasReturnValue())
                    {
                        throw new NotSupportedException(errorMessage);
                    }
                }
            }
        }

        private void AddReplaceWrapMethod(TypeDefinition targetType, ref MethodReference replaceMethodDefine, MethodDefinition eventInvoker, MethodReference rawInvoker)
        {
            MethodDefinition replaceMethodDf = eventInvoker.CopyEmptyBody(replaceMethodDefine.Name + DateTime.Now.ToString("yyyyMMddHHmmssfff"));
            ((MethodDefinition)replaceMethodDefine).SetVariables(eventInvoker.Module, replaceMethodDf);
            ((MethodDefinition)replaceMethodDefine).SetExceptionHandler(eventInvoker.Module, replaceMethodDf);
            targetType.Methods.Add(replaceMethodDf);
            ILProcessor copyToProcessor = replaceMethodDf.Body.GetILProcessor();
            foreach (var ins in ((MethodDefinition)replaceMethodDefine).Body.Instructions.ToList())
            {
                bool isCallingMethod = false;
                if (ins.Operand is MethodReference)
                {
                    var operand = (MethodReference)ins.Operand;
                    if (operand.IsGenericInstance)
                    {
                        if (operand.GetElementMethod().GetFullName() == replaceMethodDefine.GetFullName())
                        {
                            isCallingMethod = true;
                        }
                    }
                    else
                    {
                        if (operand.GetFullName() == replaceMethodDefine.GetFullName())
                        {
                            isCallingMethod = true;
                        }
                    }
                }
                if (ILHelper.IsCall(ins)
                    && isCallingMethod)
                {
                    copyToProcessor.Append(copyToProcessor.Create(OpCodes.Call, MethodHelper.GetCallMethod(rawInvoker, eventInvoker)));
                }
                else
                {
                    if (ins.Operand is MemberReference && ((MemberReference)ins.Operand).Module != eventInvoker.Module)
                    {
                        copyToProcessor.Emit(ins.OpCode, asmHelper.ImportReference((MemberReference)ins.Operand, null));
                    }
                    else
                    {
                        copyToProcessor.Append(ins);
                    }
                }
            }
            replaceMethodDefine = replaceMethodDf;
        }
    }
}
